/*
 * Copyright (C) 2009 Leandro Aparecido <lehphyro@gmail.com>
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *         http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.metalframework.persistence.repository.query;

import java.lang.annotation.*;
import java.lang.reflect.*;
import java.util.*;

import javax.persistence.*;

import org.metalframework.persistence.*;
import org.metalframework.persistence.annotation.*;
import org.metalframework.persistence.repository.*;
import org.springframework.core.*;


/**
 * Parser of the {@link org.metalframework.persistence.annotation.RepositoryQuery} annotation.
 * 
 * @since 1.0
 * @see org.metalframework.persistence.repository.RepositoryAnnotationParser
 * @see org.metalframework.persistence.repository.query.RepositoryQueryAnnotationParameterNameDiscoverer
 * @see org.springframework.core.LocalVariableTableParameterNameDiscoverer
 */
public class RepositoryQueryParser extends RepositoryAnnotationParser {
	/**
	 * Creates the parser using default values.
	 */
	public RepositoryQueryParser() {
		PrioritizedParameterNameDiscoverer prioritizedParameterNameDiscoverer = new PrioritizedParameterNameDiscoverer();
		prioritizedParameterNameDiscoverer.addDiscoverer(new RepositoryQueryAnnotationParameterNameDiscoverer());
		prioritizedParameterNameDiscoverer.addDiscoverer(new LocalVariableTableParameterNameDiscoverer());
		setParameterNameDiscoverer(prioritizedParameterNameDiscoverer);
	}

	/**
	 * Parse the annotation of a field.
	 * 
	 * @param field Field to parse query parameters from.
	 * @return Parsed parameters or null if the field doesn't have supported annotations.
	 */
	public RepositoryQueryParameters parse(Field field) {
		RepositoryQueryParameters parameters = null;
		RepositoryQuery repositoryQueryAnnotation = field.getAnnotation(RepositoryQuery.class);
		if (repositoryQueryAnnotation != null) {
			parameters = parseBasicRepositoryQueryParameters(repositoryQueryAnnotation);
			setExtentByType(parameters, field.getType());
			
			if (Collection.class.isAssignableFrom(field.getType())) {
				parameters.setType(GenericCollectionTypeResolver.getCollectionFieldType(field));
			} else {
				parameters.setType(field.getType());
			}
			
			parameters.setTargetType(ElementType.FIELD);
		}
		// TODO Refactor
		OneToMany oneToManyAnnotation = field.getAnnotation(OneToMany.class);
		if (oneToManyAnnotation != null) {
			if (parameters == null) {
				parameters = new RepositoryQueryParameters();

				if (Collection.class.isAssignableFrom(field.getType())) {
					parameters.setType(GenericCollectionTypeResolver.getCollectionFieldType(field));
				} else {
					parameters.setType(field.getType());
				}
			}
			parameters.setOneToMany(true);
		}

		return parameters;
	}

	/**
	 * Parse the annotation of a method.
	 * 
	 * @param method Method to parse query parameters from.
	 * @return Parsed parameters or null if the method doesn't have supported annotations.
	 */
	public RepositoryQueryParameters parse(Method method) {
		RepositoryQueryParameters parameters = null;
		RepositoryQuery repositoryQueryAnnotation = method.getAnnotation(RepositoryQuery.class);
		if (repositoryQueryAnnotation != null) {
			parameters = parseBasicRepositoryQueryParameters(repositoryQueryAnnotation);
			setExtentByType(parameters, method.getReturnType());

			Class<?>[] parameterTypes = method.getParameterTypes();
			if (parameterTypes.length == 1) {
				parameters.setType(parameterTypes[0]);
			} else {
				throw new IllegalArgumentException("Annotated method [" + method + "] must have exactly one parameter");
			}
			
			parameters.setTargetType(ElementType.METHOD);
		}
		// TODO Refactor
		OneToMany oneToManyAnnotation = method.getAnnotation(OneToMany.class);
		if (oneToManyAnnotation != null) {
			if (parameters == null) {
				parameters = new RepositoryQueryParameters();
			}
			parameters.setOneToMany(true);
			parameters.setType(GenericCollectionTypeResolver.getCollectionReturnType(method));
		}
		
		return parameters;
	}
	
	/**
	 * Parse the annotations of a method parameters.
	 * 
	 * @param method Method to parse query parameters from.
	 * @param arguments Arguments passed on method invocation.
	 * @return Array of parameters, null positions where parameter doesn't have supported annotations.
	 */
	public RepositoryQueryParameters[] parseMethodParameters(Method method, Object[] arguments) {
		Annotation[][] parametersAnnotations = method.getParameterAnnotations();
		RepositoryQueryParameters[] parameters = new RepositoryQueryParameters[parametersAnnotations.length];
		
		for (int i = 0; i < parametersAnnotations.length; i++) {
			Annotation[] annotations = parametersAnnotations[i];
			if (annotations.length > 0) {
				for (Annotation annotation : annotations) {
					if (RepositoryQuery.class.isAssignableFrom(annotation.getClass())) {
						RepositoryQuery repositoryQueryAnnotation = (RepositoryQuery)annotation;

						RepositoryQueryParameters aux = parseBasicRepositoryQueryParameters(repositoryQueryAnnotation);
						setExtentByType(aux, method.getParameterTypes()[i]);

						aux.setType(method.getParameterTypes()[i]);
						
						if (repositoryQueryAnnotation.byExample()) {
							aux.setParameters(parseQueryParametersByExample(method, arguments, i));
						} else {
							aux.setParameters(parseQueryParameters(method, arguments));
						}
						
						aux.setTargetType(ElementType.PARAMETER);
						
						parameters[i] = aux;
					}
				}
			} else {
				parameters[i] = null;
			}
		}
		
		return parameters;
	}
	
	/**
	 * Parse basic attributes of the {@link org.metalframework.persistence.annotation.RepositoryQuery} annotation.
	 * 
	 * @param annotation Annotation to parse attributes from.
	 * @return Parsed parameters.
	 */
	protected RepositoryQueryParameters parseBasicRepositoryQueryParameters(RepositoryQuery annotation) {
		RepositoryQueryParameters parameters = new RepositoryQueryParameters();
		parameters.setExtent(annotation.extent());
		parameters.setQueryName(parseQueryName(annotation.value(), annotation.name()));
		parameters.setFetchStrategy(annotation.fetch());
		parameters.setFetchSize(annotation.fetchSize());

		return parameters;
	}

	protected void setExtentByType(RepositoryQueryParameters parameters, Class<?> type) {
		if (Extent.class.isAssignableFrom(type)) {
			parameters.setExtent(true);
			parameters.setExtentType(true);
		}
	}
}
